package org.apache.tomcat.maven.common.deployer;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.AuthCache;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.entity.AbstractHttpEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicAuthCache;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.maven.settings.Proxy;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.proxy.ProxyUtils;

import java.io.*;
import java.net.URL;
import java.net.URLEncoder;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Locale;

/**
 * A Tomcat manager webapp invocation wrapper.
 *
 * @author Mark Hobson (markhobson@gmail.com)
 */
public class TomcatManager {
    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------

    /**
     * The charset to use when decoding Tomcat manager responses.
     */
    private static final String MANAGER_CHARSET = "UTF-8";

    // ----------------------------------------------------------------------
    // Fields
    // ----------------------------------------------------------------------

    /**
     * The full URL of the Tomcat manager instance to use.
     */
    private URL url;

    /**
     * The username to use when authenticating with Tomcat manager.
     */
    private String username;

    /**
     * The password to use when authenticating with Tomcat manager.
     */
    private String password;

    /**
     * The URL encoding charset to use when communicating with Tomcat manager.
     */
    private String charset;

    /**
     * The user agent name to use when communicating with Tomcat manager.
     */
    private String userAgent;

    /**
     * @since 2.0
     */
    private DefaultHttpClient httpClient;

    /**
     * @since 2.0
     */
    private BasicHttpContext localContext;

    private Proxy proxy;

    /**
     * @since 2.2
     */
    private boolean verbose;

    // ----------------------------------------------------------------------
    // Constructors
    // ----------------------------------------------------------------------

    /**
     * Creates a Tomcat manager wrapper for the specified URL that uses a username of <code>admin</code>, an empty
     * password and ISO-8859-1 URL encoding.
     *
     * @param url the full URL of the Tomcat manager instance to use
     */
    public TomcatManager(URL url) {
        this(url, "admin");
    }

    /**
     * Creates a Tomcat manager wrapper for the specified URL and username that uses an empty password and ISO-8859-1
     * URL encoding.
     *
     * @param url      the full URL of the Tomcat manager instance to use
     * @param username the username to use when authenticating with Tomcat manager
     */
    public TomcatManager(URL url, String username) {
        this(url, username, "");
    }

    /**
     * Creates a Tomcat manager wrapper for the specified URL, username and password that uses ISO-8859-1 URL encoding.
     *
     * @param url      the full URL of the Tomcat manager instance to use
     * @param username the username to use when authenticating with Tomcat manager
     * @param password the password to use when authenticating with Tomcat manager
     */
    public TomcatManager(URL url, String username, String password) {
        this(url, username, password, "ISO-8859-1");
    }


    /**
     * Creates a Tomcat manager wrapper for the specified URL, username, password and URL encoding.
     *
     * @param url      the full URL of the Tomcat manager instance to use
     * @param username the username to use when authenticating with Tomcat manager
     * @param password the password to use when authenticating with Tomcat manager
     * @param charset  the URL encoding charset to use when communicating with Tomcat manager
     */
    public TomcatManager(URL url, String username, String password, String charset) {
        this(url, username, password, charset, true);
    }

    /**
     * Creates a Tomcat manager wrapper for the specified URL, username, password and URL encoding.
     *
     * @param url      the full URL of the Tomcat manager instance to use
     * @param username the username to use when authenticating with Tomcat manager
     * @param password the password to use when authenticating with Tomcat manager
     * @param charset  the URL encoding charset to use when communicating with Tomcat manager
     * @param verbose  if the build is in verbose mode (quiet mode otherwise)
     * @since 2.2
     */
    public TomcatManager(URL url, String username, String password, String charset, boolean verbose) {
        this.url = url;
        this.username = username;
        this.password = password;
        this.charset = charset;
        this.verbose = verbose;

        PoolingClientConnectionManager poolingClientConnectionManager = new PoolingClientConnectionManager();
        poolingClientConnectionManager.setMaxTotal(5);
        this.httpClient = new DefaultHttpClient(poolingClientConnectionManager);

        if (StringUtils.isNotEmpty(username)) {
            Credentials creds = new UsernamePasswordCredentials(username, password);

            String host = url.getHost();
            int port = url.getPort() > -1 ? url.getPort() : AuthScope.ANY_PORT;
            httpClient.getCredentialsProvider().setCredentials(new AuthScope(host, port), creds);

            AuthCache authCache = new BasicAuthCache();
            BasicScheme basicAuth = new BasicScheme();
            HttpHost targetHost = new HttpHost(url.getHost(), url.getPort(), url.getProtocol());
            authCache.put(targetHost, basicAuth);

            localContext = new BasicHttpContext();
            localContext.setAttribute(ClientContext.AUTH_CACHE, authCache);
        }
    }

    // ----------------------------------------------------------------------
    // Public Methods
    // ----------------------------------------------------------------------

    /**
     * Gets the full URL of the Tomcat manager instance.
     *
     * @return the full URL of the Tomcat manager instance
     */
    public URL getURL() {
        return url;
    }

    /**
     * Gets the username to use when authenticating with Tomcat manager.
     *
     * @return the username to use when authenticating with Tomcat manager
     */
    public String getUserName() {
        return username;
    }

    /**
     * Gets the password to use when authenticating with Tomcat manager.
     *
     * @return the password to use when authenticating with Tomcat manager
     */
    public String getPassword() {
        return password;
    }

    /**
     * Gets the URL encoding charset to use when communicating with Tomcat manager.
     *
     * @return the URL encoding charset to use when communicating with Tomcat manager
     */
    public String getCharset() {
        return charset;
    }

    /**
     * Gets the user agent name to use when communicating with Tomcat manager.
     *
     * @return the user agent name to use when communicating with Tomcat manager
     */
    public String getUserAgent() {
        return userAgent;
    }

    /**
     * Sets the user agent name to use when communicating with Tomcat manager.
     *
     * @param userAgent the user agent name to use when communicating with Tomcat manager
     */
    public void setUserAgent(String userAgent) {
        this.userAgent = userAgent;
    }

    /**
     * Sets the proxy to use when communicating with Tomcat manager.
     *
     * @param proxy the proxy to use when communicating with Tomcat manager
     */
    public void setProxy(Proxy proxy) {
        if (this.proxy != proxy) {
            this.proxy = proxy;
            if (httpClient != null) {
                applyProxy();
            }
        }
    }

    /**
     * {@link #setProxy(Proxy)} is called by {@link AbstractCatinalMojo#getManager()} after the constructor
     */
    private void applyProxy() {
        if (this.proxy != null) {

            ProxyInfo proxyInfo = new ProxyInfo();
            proxyInfo.setNonProxyHosts(this.proxy.getNonProxyHosts());

            if (!ProxyUtils.validateNonProxyHosts(proxyInfo, url.getHost())) {
                HttpHost proxy = new HttpHost(this.proxy.getHost(), this.proxy.getPort(), this.proxy.getProtocol());
                httpClient.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, proxy);
                if (this.proxy.getUsername() != null) {
                    httpClient.getCredentialsProvider().setCredentials(
                            new AuthScope(this.proxy.getHost(), this.proxy.getPort()),
                            new UsernamePasswordCredentials(this.proxy.getUsername(), this.proxy.getPassword()));
                }
            }
        } else {
            httpClient.getParams().removeParameter(ConnRoutePNames.DEFAULT_PROXY);
        }
    }


    /**
     * Deploys the specified WAR as a URL to the specified context path.
     *
     * @param path the webapp context path to deploy to
     * @param war  the URL of the WAR to deploy
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deploy(String path, URL war)
            throws TomcatManagerException, IOException {
        return deploy(path, war, false);
    }

    /**
     * Deploys the specified WAR as a URL to the specified context path, optionally undeploying the webapp if it already
     * exists.
     *
     * @param path   the webapp context path to deploy to
     * @param war    the URL of the WAR to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deploy(String path, URL war, boolean update)
            throws TomcatManagerException, IOException {
        return deploy(path, war, update, null);
    }

    /**
     * Deploys the specified WAR as a URL to the specified context path, optionally undeploying the webapp if it already
     * exists and using the specified tag name.
     *
     * @param path   the webapp context path to deploy to
     * @param war    the URL of the WAR to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @param tag    the tag name to use
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deploy(String path, URL war, boolean update, String tag)
            throws TomcatManagerException, IOException {
        return deployImpl(path, null, war, null, update, tag);
    }

    /**
     * Deploys the specified WAR as a HTTP PUT to the specified context path.
     *
     * @param path the webapp context path to deploy to
     * @param war  an input stream to the WAR to deploy
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deploy(String path, File war)
            throws TomcatManagerException, IOException {
        return deploy(path, war, false);
    }

    /**
     * Deploys the specified WAR as a HTTP PUT to the specified context path, optionally undeploying the webapp if it
     * already exists.
     *
     * @param path   the webapp context path to deploy to
     * @param war    an input stream to the WAR to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deploy(String path, File war, boolean update)
            throws TomcatManagerException, IOException {
        return deploy(path, war, update, null);
    }

    /**
     * Deploys the specified WAR as a HTTP PUT to the specified context path, optionally undeploying the webapp if it
     * already exists and using the specified tag name.
     *
     * @param path   the webapp context path to deploy to
     * @param war    an input stream to the WAR to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @param tag    the tag name to use
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deploy(String path, File war, boolean update, String tag)
            throws TomcatManagerException, IOException {
        return deployImpl(path, null, null, war, update, tag);
    }

    /**
     * Deploys the specified WAR as a HTTP PUT to the specified context path, optionally undeploying the webapp if it
     * already exists and using the specified tag name.
     *
     * @param path   the webapp context path to deploy to
     * @param war    an input stream to the WAR to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @param tag    the tag name to use
     * @param length the size of the war deployed
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     * @since 2.0
     */
    public TomcatManagerResponse deploy(String path, File war, boolean update, String tag, long length)
            throws TomcatManagerException, IOException {
        return deployImpl(path, null, null, war, update, tag, length);
    }

    /**
     * Deploys the specified context XML configuration to the specified context path.
     *
     * @param path   the webapp context path to deploy to
     * @param config the URL of the context XML configuration to deploy
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deployContext(String path, URL config)
            throws TomcatManagerException, IOException {
        return deployContext(path, config, false);
    }

    /**
     * Deploys the specified context XML configuration to the specified context path, optionally undeploying the webapp
     * if it already exists.
     *
     * @param path   the webapp context path to deploy to
     * @param config the URL of the context XML configuration to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deployContext(String path, URL config, boolean update)
            throws TomcatManagerException, IOException {
        return deployContext(path, config, update, null);
    }

    /**
     * Deploys the specified context XML configuration to the specified context path, optionally undeploying the webapp
     * if it already exists and using the specified tag name.
     *
     * @param path   the webapp context path to deploy to
     * @param config the URL of the context XML configuration to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @param tag    the tag name to use
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deployContext(String path, URL config, boolean update, String tag)
            throws TomcatManagerException, IOException {
        return deployContext(path, config, null, update, tag);
    }

    /**
     * Deploys the specified context XML configuration and WAR as a URL to the specified context path.
     *
     * @param path   the webapp context path to deploy to
     * @param config the URL of the context XML configuration to deploy
     * @param war    the URL of the WAR to deploy
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deployContext(String path, URL config, URL war)
            throws TomcatManagerException, IOException {
        return deployContext(path, config, war, false);
    }

    /**
     * Deploys the specified context XML configuration and WAR as a URL to the specified context path, optionally
     * undeploying the webapp if it already exists.
     *
     * @param path   the webapp context path to deploy to
     * @param config the URL of the context XML configuration to deploy
     * @param war    the URL of the WAR to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deployContext(String path, URL config, URL war, boolean update)
            throws TomcatManagerException, IOException {
        return deployContext(path, config, war, update, null);
    }

    /**
     * Deploys the specified context XML configuration and WAR as a URL to the specified context path, optionally
     * undeploying the webapp if it already exists and using the specified tag name.
     *
     * @param path   the webapp context path to deploy to
     * @param config the URL of the context XML configuration to deploy
     * @param war    the URL of the WAR to deploy
     * @param update whether to first undeploy the webapp if it already exists
     * @param tag    the tag name to use
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse deployContext(String path, URL config, URL war, boolean update, String tag)
            throws TomcatManagerException, IOException {
        return deployImpl(path, config, war, null, update, tag);
    }

    /**
     * Undeploys the webapp at the specified context path.
     *
     * @param path the webapp context path to undeploy
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse undeploy(String path)
            throws TomcatManagerException, IOException {
        return invoke("/undeploy?path=" + URLEncoder.encode(path, charset));
    }

    /**
     * Reloads the webapp at the specified context path.
     *
     * @param path the webapp context path to reload
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse reload(String path)
            throws TomcatManagerException, IOException {
        return invoke("/reload?path=" + URLEncoder.encode(path, charset));
    }

    /**
     * Starts the webapp at the specified context path.
     *
     * @param path the webapp context path to start
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse start(String path)
            throws TomcatManagerException, IOException {
        return invoke("/start?path=" + URLEncoder.encode(path, charset));
    }

    /**
     * Stops the webapp at the specified context path.
     *
     * @param path the webapp context path to stop
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse stop(String path)
            throws TomcatManagerException, IOException {
        return invoke("/stop?path=" + URLEncoder.encode(path, charset));
    }

    /**
     * Lists all the currently deployed web applications.
     *
     * @return the list of currently deployed applications
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse list()
            throws TomcatManagerException, IOException {
        return invoke("/list");
    }

    /**
     * Lists information about the Tomcat version, OS, and JVM properties.
     *
     * @return the server information
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse getServerInfo()
            throws TomcatManagerException, IOException {
        return invoke("/serverinfo");
    }

    /**
     * Lists all of the global JNDI resources.
     *
     * @return the list of all global JNDI resources
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse getResources()
            throws TomcatManagerException, IOException {
        return getResources(null);
    }

    /**
     * Lists the global JNDI resources of the given type.
     *
     * @param type the class name of the resources to list, or <code>null</code> for all
     * @return the list of global JNDI resources of the given type
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse getResources(String type)
            throws TomcatManagerException, IOException {
        StringBuffer buffer = new StringBuffer();
        buffer.append("/resources");

        if (type != null) {
            buffer.append("?type=" + URLEncoder.encode(type, charset));
        }
        return invoke(buffer.toString());
    }

    /**
     * Lists the security role names and corresponding descriptions that are available.
     *
     * @return the list of security role names and corresponding descriptions
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse getRoles()
            throws TomcatManagerException, IOException {
        return invoke("/roles");
    }

    /**
     * Lists the default session timeout and the number of currently active sessions for the given context path.
     *
     * @param path the context path to list session information for
     * @return the default session timeout and the number of currently active sessions
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    public TomcatManagerResponse getSessions(String path)
            throws TomcatManagerException, IOException {
        return invoke("/sessions?path=" + URLEncoder.encode(path, charset));
    }

    // ----------------------------------------------------------------------
    // Protected Methods
    // ----------------------------------------------------------------------

    /**
     * Invokes Tomcat manager with the specified command.
     *
     * @param path the Tomcat manager command to invoke
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    protected TomcatManagerResponse invoke(String path)
            throws TomcatManagerException, IOException {
        return invoke(path, null, -1);
    }

    // ----------------------------------------------------------------------
    // Private Methods
    // ----------------------------------------------------------------------

    private TomcatManagerResponse deployImpl(String path, URL config, URL war, File data, boolean update, String tag)
            throws TomcatManagerException, IOException {
        return deployImpl(path, config, war, data, update, tag, -1);
    }

    /**
     * Deploys the specified WAR.
     *
     * @param path   the webapp context path to deploy to
     * @param config the URL of the context XML configuration to deploy, or null for none
     * @param war    the URL of the WAR to deploy, or null to use <code>data</code>
     * @param data   WAR file to deploy, or null to use <code>war</code>
     * @param update whether to first undeploy the webapp if it already exists
     * @param tag    the tag name to use
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    private TomcatManagerResponse deployImpl(String path, URL config, URL war, File data, boolean update, String tag,
                                             long length)
            throws TomcatManagerException, IOException {
        StringBuilder buffer = new StringBuilder("/deploy");
        buffer.append("?path=").append(URLEncoder.encode(path, charset));

        if (config != null) {
            buffer.append("&config=").append(URLEncoder.encode(config.toString(), charset));
        }

        if (war != null) {
            buffer.append("&war=").append(URLEncoder.encode(war.toString(), charset));
        }

        if (update) {
            buffer.append("&update=true");
        }

        if (tag != null) {
            buffer.append("&tag=").append(URLEncoder.encode(tag, charset));
        }

        return invoke(buffer.toString(), data, length);
    }


    /**
     * Invokes Tomcat manager with the specified command and content data.
     *
     * @param path the Tomcat manager command to invoke
     * @param data file to deploy
     * @return the Tomcat manager response
     * @throws TomcatManagerException if the Tomcat manager request fails
     * @throws IOException            if an i/o error occurs
     */
    protected TomcatManagerResponse invoke(String path, File data, long length)
            throws TomcatManagerException, IOException {

        HttpRequestBase httpRequestBase = null;
        if (data == null) {
            httpRequestBase = new HttpGet(url + path);
        } else {
            HttpPut httpPut = new HttpPut(url + path);

            httpPut.setEntity(new RequestEntityImplementation(data, length, url + path, verbose));

            httpRequestBase = httpPut;

        }

        if (userAgent != null) {
            httpRequestBase.setHeader("User-Agent", userAgent);
        }

        HttpResponse response = httpClient.execute(httpRequestBase, localContext);

        int statusCode = response.getStatusLine().getStatusCode();

        switch (statusCode) {
            // Success Codes
            case HttpStatus.SC_OK: // 200
            case HttpStatus.SC_CREATED: // 201
            case HttpStatus.SC_ACCEPTED: // 202
                break;
            // handle all redirect even if http specs says " the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user"
            case HttpStatus.SC_MOVED_PERMANENTLY: // 301
            case HttpStatus.SC_MOVED_TEMPORARILY: // 302
            case HttpStatus.SC_SEE_OTHER: // 303
                String relocateUrl = calculateRelocatedUrl(response);
                this.url = new URL(relocateUrl);
                return invoke(path, data, length);
        }

        return new TomcatManagerResponse().setStatusCode(response.getStatusLine().getStatusCode()).setReasonPhrase(
                response.getStatusLine().getReasonPhrase()).setHttpResponseBody(
                IOUtils.toString(response.getEntity().getContent()));

    }

    protected String calculateRelocatedUrl(HttpResponse response) {
        Header locationHeader = response.getFirstHeader("Location");
        String locationField = locationHeader.getValue();
        // is it a relative Location or a full ?
        return locationField.startsWith("http") ? locationField : url.toString() + '/' + locationField;
    }


    /**
     * Gets the HTTP Basic Authorization header value for the supplied username and password.
     *
     * @param username the username to use for authentication
     * @param password the password to use for authentication
     * @return the HTTP Basic Authorization header value
     */
    private String toAuthorization(String username, String password) {
        StringBuffer buffer = new StringBuffer();
        buffer.append(username).append(':');
        if (password != null) {
            buffer.append(password);
        }
        return "Basic " + new String(Base64.encodeBase64(buffer.toString().getBytes()));
    }

    private final class RequestEntityImplementation
            extends AbstractHttpEntity {

        private final static int BUFFER_SIZE = 2048;
        PrintStream out = System.out;
        private File file;
        private long length = -1;

        private int lastLength;

        private String url;

        private long startTime;

        private boolean verbose;

        private RequestEntityImplementation(final File file, long length, String url, boolean verbose) {
            this.file = file;
            this.length = length;
            this.url = url;
            this.verbose = verbose;
        }

        public long getContentLength() {
            return length >= 0 ? length : (file.length() >= 0 ? file.length() : -1);
        }


        public InputStream getContent()
                throws IOException, IllegalStateException {
            return new FileInputStream(this.file);
        }

        public boolean isRepeatable() {
            return true;
        }


        public void writeTo(final OutputStream outstream)
                throws IOException {
            long completed = 0;
            if (outstream == null) {
                throw new IllegalArgumentException("Output stream may not be null");
            }
            FileInputStream stream = new FileInputStream(this.file);
            transferInitiated(this.url);
            this.startTime = System.currentTimeMillis();
            try {
                byte[] buffer = new byte[BUFFER_SIZE];

                int l;
                if (this.length < 0) {
                    // until EOF
                    while ((l = stream.read(buffer)) != -1) {
                        transferProgressed(completed += buffer.length, -1);
                        outstream.write(buffer, 0, l);
                    }
                } else {
                    // no need to consume more than length
                    long remaining = this.length;
                    while (remaining > 0) {
                        int transferSize = (int) Math.min(BUFFER_SIZE, remaining);
                        completed += transferSize;
                        l = stream.read(buffer, 0, transferSize);
                        if (l == -1) {
                            break;
                        }

                        outstream.write(buffer, 0, l);
                        remaining -= l;
                        transferProgressed(completed, this.length);
                    }
                }
                transferSucceeded(completed);
            } finally {
                stream.close();
                out.println();
            }
            // end transfer
        }

        public boolean isStreaming() {
            return true;
        }


        public void transferInitiated(String url) {
            String message = "Uploading";

            out.println(message + ": " + url);
        }

        public void transferProgressed(long completedSize, long totalSize) {
            if (!verbose) {
                return;
            }

            StringBuilder buffer = new StringBuilder(64);

            buffer.append(getStatus(completedSize, totalSize)).append("  ");
            lastLength = buffer.length();
            buffer.append('\r');

            out.print(buffer);
        }

        public void transferSucceeded(long contentLength) {

            if (contentLength >= 0) {
                String type = "Uploaded";
                String len = contentLength >= 1024 ? toKB(contentLength) + " KB" : contentLength + " B";

                String throughput = "";
                long duration = System.currentTimeMillis() - startTime;
                if (duration > 0) {
                    DecimalFormat format = new DecimalFormat("0.0", new DecimalFormatSymbols(Locale.ENGLISH));
                    double kbPerSec = (contentLength / 1024.0) / (duration / 1000.0);
                    throughput = " at " + format.format(kbPerSec) + " KB/sec";
                }

                out.println(type + ": " + url + " (" + len + throughput + ")");
            }
        }

        private String getStatus(long complete, long total) {
            if (total >= 1024) {
                return toKB(complete) + "/" + toKB(total) + " KB ";
            } else if (total >= 0) {
                return complete + "/" + total + " B ";
            } else if (complete >= 1024) {
                return toKB(complete) + " KB ";
            } else {
                return complete + " B ";
            }
        }

        private long toKB(long bytes) {
            return (bytes + 1023) / 1024;
        }

    }
}
